#
# This file is a part of the open-eBackup project.
# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
# If a copy of the MPL was not distributed with this file, You can obtain one at
# http://mozilla.org/MPL/2.0/.
#
# Copyright (c) [2024] Huawei Technologies Co.,Ltd.
#
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
#

import collections
import json
import os
import pwd
import stat

from gaussdbt.commons.const import RoachConstant, ProgressInfo, AREA_PARENT_PATH
from gaussdbt.commons.database_common import check_if_path_in_cache
from common.common import execute_cmd_list, check_path_legal, check_command_injection_exclude_quote, \
    write_content_to_file
from common.const import BackupTypeEnum
from common.const import SubJobStatusEnum
from common.logger import Logger
from common.util.exec_utils import exec_mount_cmd, exec_cat_cmd, exec_umount_cmd, exec_mkdir_cmd, check_path_valid, \
    su_exec_rm_cmd, exec_overwrite_file

log = Logger().get_logger("gaussdbt_plugin.log")
NodeInfo = collections.namedtuple("NodeInfo", ['node_count', 'primary_count', 'standby_count',
                                               'mediadest_path', 'metadest_path'])


class RoachMeta:
    meta_object = None

    def __init__(self, file_path: str):
        if not file_path:
            log.error("Roach file path is null.")
            raise Exception("Roach file path is null")
        self.file_path = os.path.join(file_path, RoachConstant.ROACH_META_FILE_NAME)
        self.log_file_path = os.path.join(file_path, "roach_arch")
        if not os.path.exists(file_path):
            log.error(f"Roach file is not exit {file_path}")
            self.meta_object = None
        else:
            self.meta_object = self.read_meta_file()

    @staticmethod
    def parse_ini_get_node_info(backup_key: str, meta_path: str):
        """
        解析参数文件
        :return:
        """
        # 读取文件超时
        log.info('Get ini info')
        file_path = os.path.join(meta_path, RoachConstant.ROACH_META_FILE_NAME)
        if not os.path.exists(file_path):
            log.error(f'File: {file_path} not exists!')
            return NodeInfo(0, 0, 0, "", "")
        return_code, data = exec_cat_cmd(file_path)
        if not return_code:
            log.error(f'Get {file_path} info error')
            return NodeInfo(0, 0, 0, "", "")
        param_dict = json.loads(data)
        if not param_dict.get("BackupDetails"):
            log.error("No backups!")
            return NodeInfo(0, 0, 0, "", "")
        backup_details = param_dict.get("BackupDetails")
        for one_backup in backup_details:
            if one_backup.get("BackupKey") == backup_key:
                node_count = one_backup.get("NodeCount")
                primary_count = one_backup.get("PDNCount")
                standby_count = one_backup.get("SDNCount")
                mediadest_path = one_backup.get("MediaDestination")
                metadest_path = one_backup.get("MetaDestination")
                return NodeInfo(node_count, primary_count, standby_count, mediadest_path, metadest_path)
        log.error(f'Did not find BackupKey:{backup_key}!')
        return NodeInfo(0, 0, 0, "", "")

    def read_meta_file(self):
        try:
            with open(self.file_path, 'r', encoding='utf-8') as json_object:
                meta_object = json.loads(json_object.read())
        except IOError:
            log.error(f"Open roach meta file: {self.file_path} failed ")
            return ""
        return meta_object

    def latest_backup_key(self):
        if not self.check_meta_file():
            log.error("Roach meta file is damage.")
            raise Exception("Roach meta file is damage ")
        max_size = self.meta_object.get("BackupCount")
        backup_meta_list = self.meta_object.get("BackupDetails")
        return backup_meta_list[max_size - 1].get("BackupKey")

    def check_meta_file(self):
        if not self.meta_object:
            log.error("Roach meta info is none.")
            return False
        max_size = self.meta_object.get("BackupCount")
        backup_meta_list = self.meta_object.get("BackupDetails")
        if len(backup_meta_list) != max_size:
            log.error("Copy meta info has damage.")
            return False
        return True

    def find_copy_info(self, backup_key):
        if not self.check_meta_file():
            log.error("Roach meta file is damage.")
            raise Exception("Roach meta file is damage ")
        copy_meta_list = self.meta_object.get("BackupDetails")
        for meta_info in copy_meta_list:
            if meta_info.get("BackupKey") == backup_key:
                return meta_info
        log.error(f"Find {backup_key} copy info failed")
        return {}

    def check_copy_delete_status(self, backup_key):
        copy_info = self.find_copy_info(backup_key)
        if not copy_info:
            log.error(f"Copy info is None backup key is {backup_key}")
            return True
        if copy_info.get("STATUS") and copy_info.get("STATUS") == "DELETE":
            return True
        return False

    def check_log_copy_delete_status(self, backup_key):
        if len(backup_key.split("_")) <= 1:
            return False
        date_dir = backup_key.split("_")[0]
        log_meta_file = os.path.join(self.log_file_path, date_dir, RoachConstant.ROACH_LOG_META_FILE_NAME)
        if not os.path.exists(log_meta_file):
            log.error("Log meta data file is not exists")
            return True
        try:
            with open(log_meta_file, 'r', encoding='utf-8') as json_object:
                meta_object = json.loads(json_object.read())
        except Exception:
            log.error(f"Open roach log  meta file: {log_meta_file} failed")
            return True
        for item in meta_object.get("BackupDetails", []):
            if item.get("ArchBackupKey", "") == backup_key:
                return False
        return True


def save_full_backup_key(file_path, backup_key):
    save_backup_key(file_path, backup_key, "LastFullCopyKey")


def save_last_backup_key(file_path, backup_key):
    save_backup_key(file_path, backup_key, "LastBackupKey")


def save_backup_key(file_path, backup_key, filename):
    file_name = os.path.join(file_path, filename)
    if os.path.exists(file_name):
        log.info(f"Backup key file: {file_name} exist and remove")
        su_exec_rm_cmd(file_name)
    write_content_to_file(file_name, backup_key)


def get_full_backup_key(file_path):
    file_name = os.path.join(file_path, "LastFullCopyKey")
    with open(file_name, "r") as f:
        backup_key = f.read()
    return backup_key


def get_last_backup_key(file_path):
    file_name = os.path.join(file_path, "meta", "LastBackupKey")
    if not os.path.exists(file_name):
        log.error(f"File not exists: {file_name}")
        return ""
    with open(file_name, "r") as f:
        backup_key = f.read()
    return backup_key


def get_last_diff_backup_key(file_path):
    file_name = os.path.join(file_path, "meta", "LastDiffBackupKey")
    if not os.path.exists(file_name):
        log.error(f"File not exists: {file_name}")
        return ""
    with open(file_name, "r") as file:
        backup_key = file.read()
    return backup_key


def mount_bind_path(src_area, des_area):
    umount_bind_path(des_area)
    if check_command_injection_exclude_quote(src_area):
        log.error("The path contains special characters.")
        return False
    if not check_path_legal(src_area, AREA_PARENT_PATH):
        log.error("The path is not in the trustlist.")
        return False
    if not os.path.isdir(des_area) and not os.path.exists(des_area):
        try:
            exec_mkdir_cmd(des_area, mode=stat.S_IRWXU)
        except Exception:
            log.error("Make directory failed")
            return False
    return_code, std_out, std_err = exec_mount_cmd(src_area, des_area)
    if return_code != "0":
        log.error(f"Failed to exec mount bind path err: {std_err}!")
        return False
    log.info("Succeed to exec mount bind path")
    return True


def umount_bind_path(des_area):
    return_code, std_out, std_err = exec_umount_cmd(des_area)
    if return_code != "0":
        log.warning(f"Failed to exec umount bind path: {des_area}!")
        return False
    log.info(f"Succeed to exec umount bind path {des_area}")
    return True


def mount_bind_backup_path(data_area, meta_area=None, log_area=None, backup_type=BackupTypeEnum.FULL_BACKUP):
    if backup_type == BackupTypeEnum.LOG_BACKUP:
        if not log_area:
            log.error("Mount bind log path is none")
            return False
        if not mount_bind_path(log_area, RoachConstant.ROACH_LOG_FILE_PATH):
            log.error("Mount bind log path failed")
            return False
    if not mount_bind_path(data_area, RoachConstant.ROACH_DATA_FILE_PATH):
        log.error("Mount bind data path failed")
        return False
    if meta_area:
        if not mount_bind_path(meta_area, RoachConstant.ROACH_META_FILE_PATH):
            log.error("Mount bind meta path failed")
            return False
    log.debug("Mount bind backup path success")
    return True


def umount_bind_backup_path():
    umount_bind_path(RoachConstant.ROACH_DATA_FILE_PATH)
    umount_bind_path(RoachConstant.ROACH_LOG_FILE_PATH)
    umount_bind_path(RoachConstant.ROACH_META_FILE_PATH)


def umount_bind_backup_path_for_delete(file_path):
    umount_bind_path(RoachConstant.ROACH_DATA_DEL_FILE_PATH)
    if os.path.exists(file_path) and os.path.ismount(RoachConstant.ROACH_LOG_FILE_PATH):
        umount_bind_path(RoachConstant.ROACH_LOG_FILE_PATH)
        try:
            if check_path_valid(file_path):
                os.rmdir(file_path)
        except Exception as remove_err:
            log.error(f"Failed to remove directory, err is: {remove_err}")
    umount_bind_path(RoachConstant.ROACH_META_DEL_FILE_PATH)


def write_progress_file(message: str, file_name: str):
    log.info(f'Write message into progress file: {message}')
    exec_overwrite_file(file_name, message, json_flag=False)


def read_gauss_progress(progress_file: str, cache_path: str):
    """
    解析前置任务进度
    :return:
    """
    log.info('Query restore progress!')
    progress = 0
    status = SubJobStatusEnum.RUNNING.value
    modes = os.F_OK | os.R_OK | os.W_OK
    if not os.access(progress_file, modes):
        log.error(f"Progress file: {progress_file} not exist")
        status = SubJobStatusEnum.FAILED.value
        return progress, status
    log.debug(f'Progress file: {progress_file} exist')
    with open(progress_file, "r", encoding='UTF-8') as f:
        data = f.read()
    if ProgressInfo.FAILED in data:
        progress = 0
        status = SubJobStatusEnum.FAILED.value
        if check_if_path_in_cache(progress_file, cache_path):
            os.remove(progress_file)
        return progress, status
    if ProgressInfo.SUCCEED in data:
        progress = 100
        status = SubJobStatusEnum.COMPLETED.value
        if check_if_path_in_cache(progress_file, cache_path):
            os.remove(progress_file)
        return progress, status
    return progress, status
